﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Daniel Vaughan. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com" />
	<CreationDate>2011-11-17 16:09:31Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
using System.Windows.Navigation;

using DanielVaughan.ComponentModel.InputValidation;
using DanielVaughan.ComponentModel.StatePreservation;
using DanielVaughan.Data.Serialization;
using DanielVaughan.NavigationModel;
using DanielVaughan.Net;
using DanielVaughan.Reflection;
using DanielVaughan.Services;
using DanielVaughan.Services.Implementation;

namespace DanielVaughan.ComponentModel
{
	public interface IViewModel
	{
		string Title { get; }
	}

	public interface ICompositeViewModel
	{
		IEnumerable<ViewModelBase> ChildViewModels
		{
			get;
		}

		void ActivateViewModel(ViewModelBase viewModel);
	}

	public abstract class ViewModelBase
		: ObservableObject, IViewModel, INotifyDataErrorInfo, 
			IValidateData, IStatePreservation, INavigationAware
	{
		readonly DataErrorNotifier dataErrorNotifier;

		protected ViewModelBase()
		{
			dataErrorNotifier = new DataErrorNotifier(this, this);
			ReadStateAttributes();
			ReadValidationAttributes();
		}

		protected ViewModelBase(string title)
			: this()
		{
			this.title = title;
		}

		string title;

		public string Title
		{
			get
			{
				return title;
			}
			set
			{
				Assign("Title", ref title, value);
			}
		}

		bool busy;

		/// <summary>
		/// Indicates when the viewmodel is busy processing, 
		/// and a progress indicator should be displayed.
		/// </summary>
		public bool Busy
		{
			get
			{
				return busy;
			}
			protected set
			{
				Assign("Busy", ref busy, value);
			}
		}

		string busyMessage;

		/// <summary>
		/// The message displayed when the viewmodel is busy.
		/// </summary>
		public string BusyMessage
		{
			get
			{
				return busyMessage;
			}
			set
			{
				Assign("BusyMessage", ref busyMessage, value);
			}
		}

		protected IMessageService MessageService
		{
			get
			{
				return Dependency.Resolve<IMessageService>();
			}
		}

		public IMarketplaceService MarketplaceService
		{
			get
			{
				var marketplaceService = Dependency.Resolve<IMarketplaceService, MarketplaceService>();
				return marketplaceService;
			}
		}

		#region Input Validation
		/// <summary>
		/// Gets the property errors. This should be overriden in a subclass
		/// to provide property validation.
		/// </summary>
		/// <param name="propertyName">Name of the property to validate.</param>
		/// <param name="value">The proposed value of a property.</param>
		/// <returns>A list of validation errors; 
		/// an empty list if no errors are found.</returns>
		protected virtual IEnumerable<DataValidationError> GetPropertyErrors(
			string propertyName, object value)
		{
			return new List<DataValidationError>();
		}

		/// <summary>
		/// Adds the property to the list of known class properties, 
		/// which is used, for example, when performing validation 
		/// of the whole class instance.
		/// </summary>
		/// <param name="name">The name of the property.</param>
		/// <param name="propertyFunc">The <c>Func</c> to 
		/// retrieve the property.</param>
		protected void AddValidationProperty(
			string name, Func<object> propertyFunc)
		{
			dataErrorNotifier.AddValidationProperty(name, propertyFunc);
		}

		protected void AddValidationProperty(Expression<Func<object>> expression)
		{
			PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;
			Func<object> getter = (Func<object>)Delegate.CreateDelegate(
										typeof(Func<object>),
										this,
										propertyInfo.GetGetMethod());

			dataErrorNotifier.AddValidationProperty(name, getter);
		}

		/// <summary>
		/// Determines whether this viewmodel contains property errors. 
		/// This is done by checking it for data validation errors.
		/// </summary>
		public void IsComplete(Action isCompleteAction,
			Action notCompleteAction, Action<Exception> unknownAction)
		{
			dataErrorNotifier.IsComplete(
				isCompleteAction, notCompleteAction, unknownAction);
		}

		public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
		{
			add
			{
				dataErrorNotifier.ErrorsChanged += value;
			}
			remove
			{
				dataErrorNotifier.ErrorsChanged -= value;
			}
		}

		public IEnumerable GetErrors(string propertyName)
		{
			return dataErrorNotifier.GetErrors(propertyName);
		}

		public bool HasErrors
		{
			get
			{
				return dataErrorNotifier.HasErrors;
			}
		}

		public virtual void BeginValidation(string memberName, object value)
		{
			IEnumerable<DataValidationError> errors;
			try
			{
				errors = GetPropertyErrors(memberName, value);
			}
			catch (Exception ex)
			{
				OnValidationComplete(
					new ValidationCompleteEventArgs(memberName, ex));
				return;
			}
			OnValidationComplete(
					new ValidationCompleteEventArgs(memberName, errors));
		}

		public event EventHandler<ValidationCompleteEventArgs> ValidationComplete;

		protected virtual void OnValidationComplete(ValidationCompleteEventArgs e)
		{
			ValidationComplete.Raise(this, e);
		}

		void ReadValidationAttributes()
		{
			var properties = GetType().GetProperties();

			foreach (PropertyInfo propertyInfo in properties)
			{
				object[] attributes = propertyInfo.GetCustomAttributes(
											typeof(ValidateAttribute), true);

				if (attributes.Length <= 0)
				{
					continue;
				}

				ValidateAttribute attribute = (ValidateAttribute)attributes[0];

				if (!propertyInfo.CanRead)
				{
					throw new InvalidOperationException(string.Format(
						"Property {0} must have a getter to be validated.",
						propertyInfo.Name));
				}

				/* Prevents access to internal closure warning. */
				PropertyInfo info = propertyInfo;

				AddValidationProperty(
					propertyInfo.Name, () => info.GetValue(this, null));
			}
		}
		#endregion

		#region State Management

		protected byte[] Serialize(object value)
		{
			byte[] state = SilverlightSerializer.Serialize(value);
			return state;
		}

		protected T Deserialize<T>(byte[] data) where T : class
		{
			T result = SilverlightSerializer.Deserialize<T>(data);
			return result;
		}

		protected void RegisterStatefulProperty<TProperty>(
			ApplicationStateType applicationStateType,
			Expression<Func<TProperty>> expression,
			Action<TProperty> setAction = null)
		{
			RegisterStatefulProperty((name, getter, setter)
				=> viewState.RegisterState(
							name, getter, setter, applicationStateType),
						expression, setAction);
		}

		protected void DeregisterStatefulProperty(
			ApplicationStateType? applicationStateType,
			Expression<Func<object>> expression)
		{
			PropertyInfo propertyInfo
				= PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;
			viewState.DeregisterState(name, applicationStateType);
		}

		protected void RegisterState<T>(
			string stateKey,
			Func<T> getFunc,
			Action<T> setAction,
			ApplicationStateType applicationStateType)
		{
			viewState.RegisterState(
				stateKey, getFunc, setAction, applicationStateType);
		}

		protected void DeregisterState<T>(
			string stateKey, ApplicationStateType? applicationStateType = null)
		{
			viewState.DeregisterState(stateKey, applicationStateType);
		}

		void RegisterStatefulProperty<T>(
			Action<string, Func<T>, Action<T>> registerAction,
			Expression<Func<T>> expression, Action<T> setAction = null)
		{
			ArgumentValidator.AssertNotNull(registerAction, "registerAction");
			ArgumentValidator.AssertNotNull(expression, "expression");

			PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;
			var propertyGetterFunc = propertyInfo.CreateGetter<T>(this);

			if (setAction == null)
			{
				try
				{
					setAction = propertyInfo.CreateSetter<T>(this);
				}
				catch (Exception ex)
				{
					string message = string.Format(
						"Unable to get setter for property '{0}' {1} ", name, ex);
					Console.WriteLine(message);
					Debug.Assert(false, message);
					return;
				}
			}
			registerAction(name, propertyGetterFunc, setAction);
		}

		readonly ViewState viewState = new ViewState();

		public virtual void LoadState(
			IDictionary<string, object> persistentStateDictionary,
			IDictionary<string, object> transientStateDictionary,
			bool shouldLoadTransientState)
		{
			viewState.LoadPersistentState(persistentStateDictionary);
			if (shouldLoadTransientState)
			{
				viewState.LoadTransientState(transientStateDictionary);
			}
		}

		public virtual void SaveState(
			IDictionary<string, object> persistentStateDictionary,
			IDictionary<string, object> transientStateDictionary)
		{
			viewState.SavePersistentState(persistentStateDictionary);
			viewState.SaveTransientState(transientStateDictionary);
		}

		void ReadStateAttributes()
		{
			var properties = GetType().GetProperties();

			foreach (PropertyInfo propertyInfo in properties)
			{
				object[] attributes = propertyInfo.GetCustomAttributes(
											typeof(StatefulAttribute), true);

				if (attributes.Length <= 0)
				{
					continue;
				}

				StatefulAttribute attribute
					= (StatefulAttribute)attributes[0];
				var persistenceType = attribute.StateType;

				if (!propertyInfo.CanRead || !propertyInfo.CanWrite)
				{
					throw new InvalidOperationException(string.Format(
						"Property {0} must have a getter and a setter to be persisted.",
						propertyInfo.Name));
				}

				/* Prevents access to internal closure warning. */
				PropertyInfo info = propertyInfo;

				viewState.RegisterState(
					propertyInfo.Name,
					() => info.GetValue(this, null),
					obj => info.SetValue(this, obj, null),
					persistenceType);
			}
		}

		#endregion

		#region Navigation 
		/* (see INavigationService and its FrameNavigationService implementation, and INavigationAware) */

		INavigationService GetNavigationService()
		{
			var navigationService = Dependency.Resolve<INavigationService, FrameNavigationService>();
			return navigationService;
		}

		/// <summary>
		/// Navigates to the previous page.
		/// </summary>
		protected void GoBack()
		{
			var navigationService = GetNavigationService();
			navigationService.GoBack();
		}

		/// <summary>
		/// Navigates to the specified URI.
		/// </summary>
		/// <param name="source">The source URI.</param>
		protected void Navigate(Uri source)
		{
			var navigationService = GetNavigationService();
			navigationService.Navigate(source);
		}

		/// <summary>
		/// Navigates to the specified relative URL.
		/// The URL should be relative to the root of the application 
		/// and begin with a slash e.g., "/Views/Settings.xaml".
		/// </summary>
		/// <param name="relativeUrl">The relative URL.</param>
		protected void Navigate(string relativeUrl)
		{
			INavigationService navigationService = GetNavigationService();
			navigationService.Navigate(relativeUrl);
		}

		protected event EventHandler<EventArgs> NavigatedTo;

		protected void OnNavigatedTo(EventArgs e)
		{
			var eventCopy = NavigatedTo;
			if (eventCopy != null)
			{
				eventCopy(this, e);
			}
		}

		IDictionary<string, string> queryParameters = new Dictionary<string, string>();

		protected IDictionary<string, string> QueryParameters
		{
			get
			{
				return queryParameters;
			}
		}

		void INavigationAware.HandleNavigatedTo(NavigationEventArgs e)
		{
			if (e.Uri != null)
			{
				string decodedUrl = HttpUtility.UrlDecode(e.Uri.ToString());
				queryParameters = HttpUtilityExtended.ParseQueryString(decodedUrl);
			}
			OnNavigatedTo(EventArgs.Empty);
		}

		protected event EventHandler<NavigatingFromEventArgs> NavigatingFrom;

		protected void OnNavigatingFrom(NavigatingFromEventArgs e)
		{
			var eventCopy = NavigatingFrom;
			if (eventCopy != null)
			{
				eventCopy(this, e);
			}
		}

		void INavigationAware.HandleNavigatingFrom(NavigatingCancelEventArgs e)
		{
			var navigationService = Dependency.Resolve<INavigationService, FrameNavigationService>();
			if (e.Uri.Equals(navigationService.Source))
			{
				return;
			}

			NavigatingFromEventArgs args = new NavigatingFromEventArgs(e.NavigationMode, e.Uri);
			OnNavigatingFrom(args);
			if (args.Cancel)
			{
				e.Cancel = true;
			}
		}

		#endregion
	}

	public class NavigatingFromEventArgs : CancelEventArgs
	{
		public NavigationMode NavigationMode { get; private set; }
		public Uri Uri { get; private set; }

		public NavigatingFromEventArgs(NavigationMode navigationMode, Uri uri)
		{
			NavigationMode = navigationMode;
			Uri = uri;
		}
	}
}
